// A no-license-but-still-radio'ing CW transceiver.
// [Used to] make use of nRF24L01+ radio module's ability to send an
// unmodulated carrier wave. (And being 5V-tolerant is a win too for an easy
// Arduino project.) Jaycar in Australia sell the nRF24 with the catalogue#
// XC4508.
// --
// Depends on the RF24 library available at
// https://nrf24.github.io/RF24/index.html
// --
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
// Locate a copy of the GNU General Public License for further details.
// --
// First release 2021-07-11
// Copyright 2021 Chris Baird <chris.j.baird@gmail.com> <vk2cjb@gmail.com>

// The wiring for the nRF24L01+ module
//   nRF24 Arduino
//   ----- -------
//   VCC   3.3V
//   GND   GND
//   MO    MOSI (usually 11 on most Arduinos)
//   MI    MISO (usually 12)
//   SCK   SCLK (usually 13)
//   CE    9    (or at least consistent with what's below)
//   CSN   10
// No 3V/5V level shifting required! \o/

#include <SPI.h>
#include "nRF24L01.h" // xxx: the Arduino IDE might need angle-brackets
#include "RF24.h"

// Only channels 0 to 82 are legal 2.4GHz ISM in Australia
#define CHANNEL (81)	      // something the local WiFi doesn't spam
RF24 radio (9, 10);	      // CE, CSN

#define straightpin (4)
#define sideditpin (4)
#define sidedahpin (5)
#define buzzerpin (3)

#define INFOHZ (1000)		// "information" tone
#define BEEPHZ (700)		// our operating sidetone
#define BFOHZ  (708)		// the remote station's sidetone

// Hold the side-key at 'dit' at startup for keyer mode.
// Continue to cycle through keyer speeds.

enum { STRAIGHT = 0, SIDE = 1};	// Morse key types
int keymode = SIDE;
const char wpmlist[] = { 10, 15, 20, 25, 30, 5 };
int wpm = wpmlist[0];
int dittime = 2000 / wpm;

extern volatile unsigned long timer0_millis;

//#define DEBUG(x) x
#define DEBUG(x)

// ----------------------------------------------------------------------

void beep (int hz)
{
  static int cur = 0;
  // only actioned on the edges to improve audio quality
  if (hz != cur)
    {
      if (hz)
	tone (buzzerpin, hz);
      else
	noTone (buzzerpin);
    }
  cur = hz;
}


#define MAXHEAPSIZE (8)

typedef struct { unsigned long timestamp; int freq; } heap_s;

int heapsize = 0;
heap_s heap[MAXHEAPSIZE];


void add_beep (unsigned long t, int hz)
{
  if (heapsize == MAXHEAPSIZE) return;

  int i = heapsize;
  heapsize++;

  heap[i].timestamp = t;
  heap[i].freq = hz;

  while ((i > 0) && (heap[i].timestamp < heap[i-1].timestamp))
    {
      // todo: what issues if timestamps are the same
      unsigned long t = heap[i].timestamp;
      int f = heap[i].freq;
      heap[i].timestamp = heap[i-1].timestamp;
      heap[i].freq = heap[i-1].freq;
      heap[i-1].timestamp = t;
      heap[i-1].freq = f;
      i--;
    }
}


void check_beeps (void)
{
  unsigned long now = millis();
  static unsigned long last = 0;

  if (heapsize == 0) return;
  if (heap[0].timestamp > now) return;
  if (now == last) return;

  beep (heap[0].freq);

  for (int i = 0; i < heapsize; i++)
    {
      heap[i].timestamp = heap[i+1].timestamp;
      heap[i].freq = heap[i+1].freq;
    }
  heapsize--;

  last = now;
}

// ----------------------------------------------------------------------

void check_rx (void)
{
  if (radio.available())
    {
      unsigned long now = millis();
      uint32_t p;

      radio.read (&p, 4);
      //DEBUG (Serial.print ("rx: ");)
	//DEBUG (Serial.println (p, HEX);)

      if (p == 0xFFFFFFFFL)	// straight-key down
	{
	  add_beep (now, BFOHZ);
	  return;
	}

      if (p == 0)		// straight-key up
	{
	  add_beep (now, 0);
	  return;
	}

      // it's a side-key event then..
      unsigned long t = (unsigned long)p & 0x7FFFFFFFL;

      if ((unsigned long)p != t) // keys-down event (bit 32 set)
	{
	  // Arduinos drift like Fujiwara. Needs to be done constantly!
	  // keys-down events will be 'now'.
	  DEBUG (Serial.print("system clock adjusted: "));
	  DEBUG (Serial.print(t));
	  DEBUG (Serial.write(32));
	  DEBUG (Serial.println(millis()));
	  noInterrupts ();
	  timer0_millis = t + 0;
	  interrupts ();

	  add_beep (t, BFOHZ);
	}
      else
	// keys-up event received; future beep disable
	add_beep (t, 0);
    }
}


void tx (unsigned long timestamp, int dah)
{
  uint32_t p = timestamp & 0x7FFFFFFFL;

  if (dah) p |= 0x80000000L;

  DEBUG (Serial.print ("tx: "));
  DEBUG (Serial.println (p, HEX));

  radio.stopListening();
  radio.write (&p, 4, 1);
  radio.write (&p, 4, 1);
  radio.startListening();
}

// ----------------------------------------------------------------------
// Idea from 'Efficient Storage Of Morse Code Character Codes',
// Lawrence Krakauer, page 36, Issue 14, BYTE 1976-10
// Hardcoded strings of ".-","-...","-.-." make the Baby Alan Turing cry.

const uint8_t morse_table[] =
  {
    'K', 0b1101, 'S', 0b1000, 'X', 0b11001,
    //'A', 0b110,    'B', 0b10001,  'C', 0b10101,  'D', 0b1001,
    //'E', 0b10,     'F', 0b10100,  'G', 0b1011,   'H', 0b10000,
    //'I', 0b100,    'J', 0b11110,  'K', 0b1101,   'L', 0b10010,
    //'M', 0b111,    'N', 0b101,    'O', 0b1111,   'P', 0b10110,
    //'Q', 0b11011,  'R', 0b1010,   'S', 0b1000,   'T', 0b11,
    //'U', 0b1100,   'V', 0b11000,  'W', 0b1110,   'X', 0b11001,
    //'Y', 0b11101,  'Z', 0b10011,  '0', 0b111111, '1', 0b111110,
    //'2', 0b111100, '3', 0b111000, '4', 0b110000, '5', 0b100000,
    //'6', 0b100001, '7', 0b100011, '8', 0b100111, '9', 0b101111,
    //',', 0b1110011,'.', 0b1101010,'?', 0b1001100,'/', 0b101001,
    //'=', 0b110001, '-', 0b1100001,':', 0b1000111,';', 0b1010101,
    0, 0
  };

void morseout (const char* s)
{
  int i;

  while (*s)
    {
      for (i = 0; morse_table[i]; i += 2)
	if (morse_table[i] == *s) break;

      if (morse_table[i])
	{
	  uint8_t b = morse_table[i+1];

	  while (b != 1)
	    {
	      beep (INFOHZ);
	      if (b & 1)
		delay (3*dittime); // dash (3 dits)
	      else
		delay (dittime); // dit (1 dit)
	      beep (0);
	      delay (dittime);	// inter-symbol delay (1 dit)
	      b >>= 1;
	    }
	}
      else
	delay (5*dittime); // not in table - treat as a space (7-2 dits)

      delay (2*dittime);	// intra-letter space (3-1 dits)
      s++;
    }
}

// ----------------------------------------------------------------------

const uint8_t* address3 = "CJB";

void setup(void)
{
  DEBUG (Serial.begin (115200));
  DEBUG (while (!Serial) continue);

  pinMode (straightpin, INPUT_PULLUP);
  pinMode (sideditpin, INPUT_PULLUP);
  pinMode (sidedahpin, INPUT_PULLUP);
  pinMode (buzzerpin, OUTPUT);

  while (!radio.begin())
    {
      morseout ("X");
      DEBUG (Serial.println("XXX");)
      delay (1000); //while (1) continue;
    }

  // Configuration for shortest packets possible. Milliseconds count!
  radio.setPALevel (RF24_PA_MIN, 1);
  radio.setPayloadSize (4);
  radio.setDataRate (RF24_1MBPS);
  radio.setChannel (CHANNEL);
  radio.setCRCLength (RF24_CRC_8);
  radio.setAutoAck (false);
  radio.setRetries (0,0);
  radio.setAddressWidth (3);
  radio.openWritingPipe (address3);
  radio.openReadingPipe (1, address3);
  radio.txDelay = 0;
  radio.startListening();

  keymode = digitalRead (sideditpin) ? STRAIGHT : SIDE;

  int p = 1;
  if (keymode == SIDE)
    while (1)
      {
	morseout ("K");
	delay (1000);
	if (!digitalRead (sideditpin))
	  {
	    if (p == sizeof(wpmlist)/sizeof(char)) p = 0;
	    wpm = wpmlist [p++];
	    dittime = 2000 / wpm;
	  }
	else
	  break;
      }
  else
    morseout ("S");
}

// ----------------------------------------------------------------------

#define LevelMax (15)
#define LevelHiThresh (12)
#define LevelLoThresh (4)
#define DEBOUNCE (4)

void check_keys (void)
{
  static char level = (LevelHiThresh + LevelLoThresh)/2;
  static int dur = 0;
  unsigned char k, di, da;
  unsigned long now = millis();

  if (keymode == SIDE)
    {
      di = digitalRead (sideditpin) ? 0 : 1;
      da = digitalRead (sidedahpin) ? 0 : 1;
      k = di | da;
    }
  else
    k = digitalRead (straightpin) ? 0 : 1;

  // Hysteresis-based debouncing
  // A discovered glitch was the buzzer on pin 3 interferring with the input
  // on pin 4. No, not crosstalk, not RF feedback, not a compiler bug. I
  // suspect its an Arduino core library issue; seen similar reported.
  // Anyway, this needed to filter out more noise than just the keybounce.

  if (k)
    level += (level == LevelMax) ? 0 : 1;
  else
    level -= (level == 0) ? 0 : 1;

  if ((level <= LevelLoThresh) || (level >= LevelHiThresh))
    dur += (dur > DEBOUNCE) ? 0 : 1;
  else
    dur = (dur < 0) ? dur+1 : 0;

  if (level >= LevelHiThresh) k = 1;
  if (level <= LevelLoThresh) k = 0;

  //

  if (dur == DEBOUNCE)
    {
      if ((keymode == SIDE) && k)
	{
	  unsigned long end = now + dittime;
	  dur = dur - DEBOUNCE - dittime*2;
	  if (da)
	    {
	      end += dittime*2;
	      dur -= dittime*2;
	    }

	  add_beep (now, BEEPHZ);
	  tx (now, 1);
	  add_beep (end, 0);
	  tx (end, 0);
	}

      if (keymode == STRAIGHT)
	if (k)
	  {
	    tx (0xFFFFFFFF, 1);
	    add_beep (now, BEEPHZ);
	  }
	else
	  {
	    tx (0, 0);
	    add_beep (now, 0);
	  }
    }
}

// ----------------------------------------------------------------------

void loop (void)
{
  check_keys();
  check_rx();
  check_beeps();
  delay (1);
}

// ----------------------------------------------------------------------
